﻿using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Sustainalytics.FileManager
{
    public class DiskFileManager : IFileManager
    {
        private const int _bufferSize = 100000;

        /// <summary>
        /// Directory path where files are stored.
        /// </summary>
        public string DirectoryPath { get; private set; }


        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="directoryPath"> File directory path. </param>
        public DiskFileManager(string directoryPath)
        {
            this.DirectoryPath = directoryPath;
        }


        /// <summary>
        /// Add file.
        /// </summary>
        /// <param name="file"> File contents. </param>
        public void AddFile(FileContents file)
        {
            WriteFile(file);
        }

        /// <summary>
        /// Get file contents.
        /// </summary>
        /// <param name="filePath"> File path. </param>
        /// <returns> File contents. </returns>
        public FileContents GetFile(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath");
            }


            // get file name
            string fileName = FileContents.GetPathFileName(filePath);

            // metadata file name
            string metadataFileName = string.Concat(fileName, ".json");

            // get file directory
            string dirPath = FileContents.GetPathDirectory(filePath, fileName);

            // get disk directory
            string diskDir = GetDirectoryFromPath(dirPath);

            // file path on disk
            string diskFilePath = Path.Combine(diskDir, fileName);

            // metadata path on disk
            string diskMetadataFilePath = Path.Combine(diskDir, metadataFileName);

            // read file metadata
            FileMetadata fileMetadata = GetFileMetadata(diskMetadataFilePath);

            // create file contents
            FileContents fileContents = new FileContents(
                filePath: filePath,
                metadata: fileMetadata,
                streamAccessor: () =>
                {
                    return (Stream)File.OpenRead(diskFilePath);
                });

            return fileContents;
        }

        /// <summary>
        /// Get files in path.
        /// </summary>
        /// <param name="dirPath"> File path to search for files. </param>
        /// <returns> Files. </returns>
        public string[] GetFilesInPath(string dirPath)
        {
            // search for files
            string diskDirPath = GetDirectoryFromPath(dirPath);

            // get files from directory
            if (Directory.Exists(diskDirPath))
            {
                var files = Directory.GetFiles(diskDirPath);
                return files.Select(f => new FileInfo(f).Name).ToArray();
            }
            else
            {
                return new string[0];
            }
        }

        /// <summary>
        /// Get files in path.
        /// </summary>
        /// <param name="dirPath"> File path to search for files. </param>
        /// <returns> Files. </returns>
        public FileContents[] GetFiles(string dirPath)
        {
            // get file names
            string[] fileNames = this.GetFilesInPath(dirPath);

            List<FileContents> fileContentsLst = new List<FileContents>();

            // for each file name get its content
            foreach (string fileName in fileNames)
            {
                // create file path
                string filePath = FileContents.ConcatPaths(dirPath, fileName);

                // get file contents
                FileContents fileContents = this.GetFile(filePath);

                fileContentsLst.Add(fileContents);
            }

            return fileContentsLst.ToArray();
        }


        private void WriteFile(FileContents fileContents)
        {
            if (fileContents.StreamAccessor == null)
            {
                throw new FileContentsStreamNullException();
            }

            // path file name
            string fileName = fileContents.PathFileName;

            // metadata file name
            string metadataFileName = string.Concat(fileName, ".json");

            // directory path
            string dirPath = fileContents.PathDirectory.Replace('/', '\\');

            // disk directory path
            string diskDirPath = Path.Combine(this.DirectoryPath, dirPath);

            // ensure that directory is created
            if (Directory.Exists(diskDirPath) == false)
            {
                Directory.CreateDirectory(diskDirPath);
            }

            // file path on disk
            string diskFilePath = Path.Combine(diskDirPath, fileName);

            // metadata path on disk
            string diskMetadataFilePath = Path.Combine(diskDirPath, metadataFileName);

            // clean files - main file
            if (File.Exists(diskFilePath))
            {
                File.Delete(diskFilePath);
            }

            if (File.Exists(diskMetadataFilePath))
            {
                File.Delete(diskMetadataFilePath);
            }


            // write contents to disk
            WriteFileContents(diskFilePath, fileContents.StreamAccessor);

            // write metadata to disk
            WriteFileMetadata(diskMetadataFilePath, fileContents.Metadata);
        }

        private void WriteFileContents(string diskFilePath, Func<Stream> streamAccessor)
        {
            byte[] buffer = new byte[_bufferSize];

            // write file contents to disk
            using (FileStream fs = new FileStream(diskFilePath, FileMode.Create, FileAccess.Write))
            {
                int bytesRead;

                using (Stream targetStream = streamAccessor())
                {
                    while ((bytesRead = targetStream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        fs.Write(buffer, 0, bytesRead);
                    }
                }
            }
        }

        private void WriteFileMetadata(string diskMetadataFilePath, FileMetadata metadata)
        {
            // serialize metadata to JSON
            string metadataJson = JsonConvert.SerializeObject(metadata);

            // write metadata to file
            using (FileStream fs = new FileStream(diskMetadataFilePath, FileMode.Create, FileAccess.Write))
            {
                using (StreamWriter sw = new StreamWriter(fs))
                {
                    sw.Write(metadataJson);
                }
            }
        }

        private FileMetadata GetFileMetadata(string diskMetadataFilePath)
        {
            string metadataJson;

            if (File.Exists(diskMetadataFilePath) == false)
            {
                return null;
            }

            // read metadata JSON from disk
            using (FileStream fs = File.OpenRead(diskMetadataFilePath))
            {
                using (StreamReader sr = new StreamReader(fs))
                {
                    metadataJson = sr.ReadToEnd();
                }
            }

            // deserialize metadata
            FileMetadata metadata = JsonConvert.DeserializeObject<FileMetadata>(metadataJson);

            return metadata;
        }

        private string GetDirectoryFromPath(string dirPath)
        {
            if (dirPath == null)
            {
                throw new ArgumentNullException("filePath");
            }

            dirPath = dirPath.Replace('/', '\\');

            // search for files
            string diskDirPath = Path.Combine(this.DirectoryPath, dirPath);

            // get files from directory
            if (Directory.Exists(diskDirPath))
            {
                return diskDirPath;
            }
            else
            {
                // get root directory
                string rootDir = new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).DirectoryName;

                // determine relative directory
                diskDirPath = Path.Combine(rootDir, diskDirPath);

                return diskDirPath;
            }            
        }
    }
}
